// Copyright 2012 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.api.ads.adwords.lib.utils;

import com.google.api.ads.adwords.lib.client.AdWordsSession;
import com.google.api.ads.common.lib.exception.AuthenticationException;
import com.google.api.ads.common.lib.utils.Streams;
import com.google.common.annotations.VisibleForTesting;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Helper class that handles AdHoc report downloads. Requests will be logged (header and payload) to
 * INFO. Successful reports will be logged (headers and response code) to INFO. Failed reports will
 * be logged in their entirety to WARN.
 *
 * @author Kevin Winter
 */
public class AdHocReportDownloadHelper {

  private static final Logger logger = LoggerFactory.getLogger(
      AdHocReportDownloadHelper.class.getPackage().getName() + ".report_download");

  /** Regular expression used to detect errors messages in a response. */
  private static final String ERROR_MESSAGE_REGEX =
      "^.*!!![^|]*\\|\\|\\|([^|]*)\\|\\|\\|([^?]*)\\?\\?\\?.*$";

  /** Regex group number that report exception message is stored in. */
  private static final int ERROR_MESSAGE_GROUP = 2;

  private final AdWordsSession session;
  private final ReportUrlConnectionHelper reportUrlConnectionHelper;
  private final String version;

  /**
   * Constructor that stores the session for authentication and uses the provided version to
   * determine the report endpoint.
   */
  public AdHocReportDownloadHelper(AdWordsSession session, String version) {
    this.session = session;
    this.version = version;
    this.reportUrlConnectionHelper = new ReportUrlConnectionHelper(session);
  }

  /**
   * Downloads a report and returns a ReportDownloadResponse with the results.
   *
   * @param reportDefinitionXml to download a report for.
   * @return {@link ReportDownloadResponse} When HTTP request completes. On
   *         success, the outputStream will be flushed and closed.
   * @throws ReportException If there is any issue making HTTP request with
   *         server.
   */
  public ReportDownloadResponse downloadReport(String reportDefinitionXml) throws ReportException {
    try {
      String downloadUrl = generateReportUrl(version);

      HttpURLConnection conn = reportUrlConnectionHelper.getReportHttpUrlConnection(downloadUrl);

      conn.setRequestProperty("Content-type", "application/x-www-form-urlencoded");
      logger.info("Content-type: {}", "application/x-www-form-urlencoded");
      conn.setRequestMethod("POST");
      conn.setDoOutput(true);

      writeReportToStream(conn.getOutputStream(), reportDefinitionXml);
      logger.info(reportDefinitionXml);

      int status = conn.getResponseCode();
      String responseMessage = "SUCCESS";
      InputStream inputStream = null;
      if (status == HttpURLConnection.HTTP_OK) {
        inputStream = conn.getInputStream();
        logger.info(conn.getResponseMessage());
        logResponseHeaders(conn.getHeaderFields(), true);
      } else {
        logger.warn(conn.getResponseMessage());
        logResponseHeaders(conn.getHeaderFields(), false);
        // Anything other than success means the body has an error message.
        responseMessage = Streams.readAll(conn.getErrorStream());
        Matcher matcher = Pattern.compile(ERROR_MESSAGE_REGEX).matcher(responseMessage);
        if (matcher.matches()) {
          responseMessage = matcher.group(ERROR_MESSAGE_GROUP);
        }
        logger.warn(responseMessage);
      }

      return new ReportDownloadResponse(status, responseMessage, inputStream);
    } catch (MalformedURLException e) {
      throw new ReportException("Created invalid report download URL.", e);
    } catch (IOException e) {
      throw new ReportException("Problem sending data to report download server.", e);
    } catch (AuthenticationException e) {
      throw new ReportException("Unable to authenticate.", e);
    }
  }

  /**
   * Logs the response headers.
   */
  @VisibleForTesting
  void logResponseHeaders(Map<String, List<String>> headerFields, boolean success) {
    for (Map.Entry<String, List<String>> header : headerFields.entrySet()) {
      String headerName = header.getKey();
      for (String value : header.getValue()) {
        if (success) {
          logger.info("{}: {}", headerName, value);
        } else {
          logger.warn("{}: {}", headerName, value);
        }
      }
    }
  }

  /**
   * Serializes the report to XML and writes it form url-encoded to the provided stream.
   *
   * @param outputStream to write to.
   * @param reportDefinitionXml to write.
   * @throws IOException if there is a problem writing to the output stream.
   * @throws UnsupportedEncodingException if unicode is not supported.
   */
  @VisibleForTesting
  void writeReportToStream(OutputStream outputStream, String reportDefinitionXml)
      throws UnsupportedEncodingException, IOException {
    OutputStreamWriter writer = new OutputStreamWriter(outputStream);
    writer.write("__rdxml=" + URLEncoder.encode(reportDefinitionXml, "UTF-8"));
    writer.close();
  }

  /**
   * Creates the report download URL.
   *
   * @param version to download from.
   * @return url to download a report from.
   */
  @VisibleForTesting
  String generateReportUrl(String version) {
    return session.getEndpoint() + ReportUrlConnectionHelper.DOWNLOAD_SERVER_URI + '/' + version;
  }
}
